package theeg.DependencyAnalysis.Core.DataModel;

import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static LEX.LanguageExtensions.as;
import static LEX.LanguageExtensions.lock;

public static class DescriptorBuilder
{
    // TODO replace those two by things exisiting in JRE
    public static final String LinqIdentifier = "DOES NO EXIT";
    private static final String GenericIdentifier = "DOES NOT EXIST";

    private static final Lock LockCache = new ReentrantLock();

    private static final Map<String, ModuleDescriptor> PackageCache = new HashMap<>();
    private static final Map<String, MemberDescriptor> MemberCache = new HashMap<>();
    private static final Map<String, TypeDescriptor> TypeCache = new HashMap<>();
    private static final Map<String, SubmoduleDescriptor> NamespaceCache = new HashMap<>();
    private static final Map<String, DependencyDescriptor> DependencyCache = new HashMap<>();

    /// <summary>
    /// Empty the caches containing found descriptors for modules, submodules,
    /// types, members and dependencies. Before analyzing of a project starts the caches
    /// are cleared.
    /// </summary>
    public static void flushCache()
    {
        lock(LockCache, () ->
        {
            DependencyCache.clear();
            PackageCache.clear();
            MemberCache.clear();
            TypeCache.clear();
            NamespaceCache.clear();
        });
    }

    /// <summary>
    /// Create a descriptor for the requested assembly. Submodules are not filled nor are any properties of
    /// ModuleDescriptor set that require analyzing the target. When the descriptor is already cached then
    /// that version of returned.
    /// </summary>
    /// <param name="module">an assembly</param>
    /// <returns></returns>
    public static ModuleDescriptor buildModuleDescriptor(final Package module)
    {
        final String packageIdentifier = createAssemblyIdentifier(module);

        final ModuleDescriptor[] moduleDescriptor = {null};

        lock(LockCache, () ->
        {
            if (PackageCache.containsKey(packageIdentifier))
            {
                moduleDescriptor[0] = PackageCache.get(packageIdentifier);
                return;
            }
            boolean isPartOfFramework = packageIsPartOfFramework(module);
            moduleDescriptor[0] = new ModuleDescriptor();
            moduleDescriptor[0].setIdentifier(packageIdentifier);
            moduleDescriptor[0].setFullName(module.getName());
            moduleDescriptor[0].setPartOfFramework(isPartOfFramework);
            PackageCache.put(packageIdentifier, moduleDescriptor[0]);
        });
        return moduleDescriptor[0];
    }

    private static boolean packageIsPartOfFramework(Package module)
    {
        return false;
    }

    private static String createAssemblyIdentifier(Package module)
    {
        String name = module.getName();
        return name + " @ " + module.getImplementationVersion();
    }

    private static NodeVisibility determineVisibilityForMember(Member member)
    {
        Method methodInfo = as(member, Method.class);
        if (methodInfo != null) return DetermineVisibilityForMember(methodInfo);
        Field fieldInfo = as(member, Field.class);
        if (fieldInfo != null) return determineVisibilityForMember(fieldInfo);
        return NodeVisibility.Ignore;
    }

    private static NodeVisibility DetermineVisibilityForMember(Method member)
    {
        int modifiers = member.getModifiers();
        if (Modifier.isPublic(modifiers)) return NodeVisibility.Public;
        if (Modifier.isPrivate(modifiers)) return NodeVisibility.Private;
        if (member.IsFamilyAndAssembly) return NodeVisibility.Protected | NodeVisibility.Internal;
        if (Modifier.isProtected(modifiers)) return NodeVisibility.Protected;
        return NodeVisibility.Protected;
    }

    private static NodeVisibility determineVisibilityForType(Class member)
    {
        int modifiers = member.getModifiers();
        if (Modifier.isPublic(modifiers)) return NodeVisibility.Public;
        if (Modifier.isPrivate(modifiers)) return NodeVisibility.Private;
        if (Modifier.isProtected(modifiers)) return NodeVisibility.Protected;
        return NodeVisibility.Ignore;
    }

    private static NodeVisibility determineVisibilityForMember(Field member)
    {
        int modifiers = member.getModifiers();
        if (Modifier.isPublic(modifiers)) return NodeVisibility.Public;
        if (Modifier.isPrivate(modifiers)) return NodeVisibility.Private;
        if (member.IsFamilyAndAssembly) return NodeVisibility.Protected | NodeVisibility.Internal;
        if (Modifier.isProtected(modifiers)) return NodeVisibility.Protected;
        return NodeVisibility.Protected;
    }

    public static String ExtractNamespaceFromType(Type type)
    {
        try
        {
            return type.Namespace;
        } catch (FileLoadException)
        {
            var fullName = type.FullName;
            var lastDot = fullName.LastIndexOf(".", StringComparison.InvariantCulture);
            return fullName.Substring(0, lastDot);
        }
    }

    /// <summary>
    /// Create a descriptor for a namespace in a module. types are not filled nor are any properties of
    /// SubmoduleDescriptor set that require analyzing the target. When the descriptor is already cached then
    /// that version of returned.
    /// </summary>
    /// <param name="moduleDescriptor"></param>
    /// <param name="typeNamespace"></param>
    /// <returns></returns>
    public static SubmoduleDescriptor BuildSubmoduleDescriptor(ModuleDescriptor moduleDescriptor, String typeNamespace)
    {
        if (typeNamespace == null)
        {
            typeNamespace = "";
        }
        typeNamespace = typeNamespace.Replace('\\', '.');
        typeNamespace = typeNamespace.Replace('/', '.');
        var namespaceIdentifier = moduleDescriptor.Identifier + " /submodule " + typeNamespace;
        lock(LockCache)
        {
            if (NamespaceCache.ContainsKey(namespaceIdentifier)) return NamespaceCache[namespaceIdentifier];
            string name = typeNamespace;
            if (moduleDescriptor.FullName != null &&
                    typeNamespace.StartsWith(moduleDescriptor.FullName))
            {
                name = typeNamespace.Substring(moduleDescriptor.FullName.Length);
                if (String.IsNullOrEmpty(name))
                {
                    name = " / ";
                }
            }
            var submoduleDescriptor = new SubmoduleDescriptor
            {
                Module = moduleDescriptor,
                        FullName = typeNamespace,
                        Name = name,
                        Identifier = namespaceIdentifier
            } ;
            NamespaceCache[namespaceIdentifier] = submoduleDescriptor;
            return submoduleDescriptor;
        }
    }

    internal

    static TypeDescriptor BuildTypeDescriptor(Type type, SubmoduleDescriptor submoduleDescriptor)
    {
        if (type.IsGenericType)
        {
            type = type.GetGenericTypeDefinition();
        }
        var module = type.Assembly;
        string fullName = ExtractTypeName(type.FullName);
        string name = ExtractTypeName(type.Name);
        var typeIdentifier = CreateAssemblyIdentifier(module) + " /type " + fullName;
        bool isLinq = !string.IsNullOrEmpty(type.FullName) && type.FullName.Contains(LinqIdentifier);
        lock(LockCache)
        {
            TypeDescriptor typeDescriptor;
            if (TypeCache.ContainsKey(typeIdentifier))
            {
                typeDescriptor = TypeCache[typeIdentifier];
            } else
            {
                typeDescriptor = new TypeDescriptor
                {
                    FullName = fullName,
                            Identifier = typeIdentifier,
                            Submodule = submoduleDescriptor,
                } ;
                TypeCache[typeIdentifier] = typeDescriptor;
            }
            if (!isLinq)
            {
                typeDescriptor.IsEnum = type.IsEnum;
                typeDescriptor.Type = type;
                typeDescriptor.Name = name;
                typeDescriptor.Visibility = DetermineVisibilityForMember((_Type) type);
                typeDescriptor.AbstractionType = DetermineAbstractionType(type);
                if (type.Name.StartsWith("<"))
                {
                    typeDescriptor.IsGenerated = true;
                }
            }
            return typeDescriptor;
        }
    }

    private static string ExtractTypeName(string name)
    {
        if (name == null) return String.Empty;
        var indexOf = name.IndexOf(LinqIdentifier);
        if (indexOf > -1)
        {
            name = name.Substring(0, indexOf);
        }
        indexOf = name.IndexOf(GenericIdentifier);
        if (indexOf > -1)
        {
            name = name.Substring(0, indexOf);
        }
        return name;
    }

    /// <summary>
/// Create a descriptor for a type. Members are not filled nor are any properties of
/// TypeDescriptor set that require analyzing the target. When the descriptor is already cached then
/// that version of returned.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
    public static TypeDescriptor BuildTypeDescriptor(Type type)
    {
        if (type.IsGenericType)
        {
            type = type.GetGenericTypeDefinition();
        }
        string fullName = ExtractTypeName(type.FullName);
        string name = ExtractTypeName(type.Name);
        var identifier = CreateAssemblyIdentifier(type.Assembly) + " /type " + fullName;
        lock(LockCache)
        {
            if (TypeCache.ContainsKey(identifier))
            {
                return TypeCache[identifier];
            }
        }
        var typeDescriptor = new TypeDescriptor
        {
            IsEnum = type.IsEnum,
                    FullName = fullName,
                    Identifier = identifier,
                    Name = name,
                    Type = type,
                    Visibility = NodeVisibility.Ignore,
                    AbstractionType = DetermineAbstractionType(type)
        } ;
        return typeDescriptor;
    }

    private static AbstractionType DetermineAbstractionType(Type type)
    {
        if (type.IsAbstract && type.IsSealed)
        {
            return AbstractionType.Static;
        }
        return type.IsInterface ? AbstractionType.Interface : type.IsAbstract ? AbstractionType.Abstract : AbstractionType.Concrete;
    }

    internal

    static DependencyDescriptor BuildDependencyDescriptor(MemberDescriptor source, MemberDescriptor target,
                                                          ReferenceType referenceType)
    {
        var identifier = referenceType + " @ " + source.Identifier + " -> " + target.Identifier;
        lock(DependencyCache)
        {
            if (DependencyCache.ContainsKey(identifier))
            {
                return DependencyCache[identifier];
            }
        }

        var dependencyDescriptor = new DependencyDescriptor
        {
            Source = source,
                    ReferenceType = referenceType,
                    Target = target,
                    Identifier = identifier
        } ;
        lock(DependencyCache)
        {
            if (!DependencyCache.ContainsKey(identifier))
            {
                return DependencyCache[identifier] = dependencyDescriptor;
            }
        }
        return dependencyDescriptor;
    }

    internal

    static DependencyDescriptor BuildDependencyDescriptorForMember(MemberInfo targetMember, MemberDescriptor memberDescriptor, ReferenceType referenceType)
    {
        var targetMemberDescriptor = BuildMemberDescriptor(targetMember);
        var identifier = referenceType + " @ " + memberDescriptor.Identifier + " -> " + targetMemberDescriptor.Identifier;
        lock(DependencyCache)
        {
            if (DependencyCache.ContainsKey(identifier))
            {
                return DependencyCache[identifier];
            }
        }
        var targetType = targetMember.ReflectedType;
        var targetNamespace = ExtractNamespaceFromType(targetType);
        var targetAssembly = targetType.Assembly;
        var targetAssemblyDescriptor = BuildModuleDescriptor(targetAssembly);
        var targetNamespaceDescriptor = BuildSubmoduleDescriptor(targetAssemblyDescriptor, targetNamespace);
        var targetTypeDescriptor = BuildTypeDescriptor(targetType, targetNamespaceDescriptor);
        targetMemberDescriptor.ImplementingType = targetTypeDescriptor;

        var dependencyDescriptor = new DependencyDescriptor
        {
            Source = memberDescriptor,
                    ReferenceType = referenceType,
                    Target = targetMemberDescriptor,
                    Identifier = identifier
        } ;
        lock(DependencyCache)
        {
            if (!DependencyCache.ContainsKey(identifier))
            {
                return DependencyCache[identifier] = dependencyDescriptor;
            }
        }
        return dependencyDescriptor;
    }

    internal

    static DependencyDescriptor BuildDependencyDescriptorForTypeReference(Type targetType, MemberDescriptor memberDescriptor, ReferenceType referenceType)
    {
        var identifier = referenceType + " @ " + memberDescriptor.Identifier + " -> " + targetType.FullName;
        lock(DependencyCache)
        {
            if (DependencyCache.ContainsKey(identifier))
            {
                return DependencyCache[identifier];
            }
        }
        var targetNamespace = ExtractNamespaceFromType(targetType);
        var targetAssembly = targetType.Assembly;
        var targetAssemblyDescriptor = BuildModuleDescriptor(targetAssembly);
        var targetNamespaceDescriptor = BuildSubmoduleDescriptor(targetAssemblyDescriptor, targetNamespace);
        var targetTypeDescriptor = BuildTypeDescriptor(targetType, targetNamespaceDescriptor);
        MemberDescriptor targetMemberDescriptor = BuildMemberDescriptor(targetType, targetTypeDescriptor, true);
        DependencyDescriptor dependencyDescriptor = new DependencyDescriptor
        {
            Source = memberDescriptor,
                    ReferenceType = referenceType,
                    Target = targetMemberDescriptor,
                    Identifier =
                            identifier
        } ;
        lock(DependencyCache)
        {
            if (!DependencyCache.ContainsKey(identifier))
            {
                return DependencyCache[identifier] = dependencyDescriptor;
            }
        }
        return dependencyDescriptor;
    }

    /// <summary>
/// Creates a descriptor for the given member. When the member is already cached then that version
/// is returned. No properties are filled that require analysis.
/// </summary>
/// <param name="member"></param>
/// <returns></returns>
    public static MemberDescriptor BuildMemberDescriptor(MemberInfo member)
    {
        return BuildMemberDescriptor(member, BuildTypeDescriptor(member.ReflectedType), false);
    }

    internal

    static MemberDescriptor BuildMemberDescriptor(MemberInfo member, TypeDescriptor typeDescriptor, bool justReference)
    {
        var toType = member as Type;
        string innerTypeName = ExtractInnerTypeName(member, toType);

        string fullname;
        Assembly assembly;
        string cacheIdentifier;
        if (toType == null)
        {
            fullname = typeDescriptor.FullName + innerTypeName + ".§" + member;
            assembly = member.ReflectedType.Assembly;
            cacheIdentifier = typeDescriptor.FullName + innerTypeName + ".§" + member.Name;
        } else
        {
            fullname = toType.FullName;
            assembly = toType.Assembly;
            cacheIdentifier = toType.FullName;
        }
        cacheIdentifier += +member.MetadataToken;
        var memberIdentifier = (justReference ? fullname + " /reference " : assembly.Location + " /member ") + fullname;
        var fieldInfo = member as FieldInfo;
        var methodInfo = member as MethodInfo;
        lock(LockCache)
        {
            if (MemberCache.ContainsKey(cacheIdentifier)) return MemberCache[cacheIdentifier];
            var name = string.IsNullOrEmpty(innerTypeName) ? member.Name : innerTypeName + ".§" + member.Name;
            MemberDescriptor memberDescriptor = new MemberDescriptor
            {
                ImplementingType = typeDescriptor,
                        FullName =
                                justReference ? typeDescriptor.FullName : fullname,
                        Identifier = memberIdentifier,
                        Visibility = DetermineVisibilityForMember(member),
                        MemberType = justReference ? MemberType.TypeReference : (fieldInfo != null ? MemberType.Field : MemberType.Method),
                        Name = justReference ? " / " : name,
                        IsAbstract = member.MemberType == MemberTypes.Method && ((MethodBase) member).IsAbstract
            } ;
            if (fieldInfo != null)
            {
                memberDescriptor.ReturningType = BuildTypeDescriptor(fieldInfo.FieldType);
            } else if (methodInfo != null)
            {
                memberDescriptor.ReturningType = BuildTypeDescriptor(methodInfo.ReturnType);
                var parameters = methodInfo.GetParameters();
                memberDescriptor.ParameterCount = parameters.Count();
            }

            MemberCache[cacheIdentifier] = memberDescriptor;
            return memberDescriptor;
        }
    }

    private static string ExtractInnerTypeName(MemberInfo member, Type toType)
    {
        string innerTypeName = string.Empty;
        if (toType == null)
        {
            var name = member.ReflectedType.Name;
            if (name.StartsWith("<>"))
            {
                innerTypeName = "+" + name;
            }
        }
        return innerTypeName;
    }

    public static MemberDescriptor BuildXamlMemberDescriptor(TypeDescriptor xamlTypeDescriptor, string name)
    {
        var isTypeMember = String.IsNullOrEmpty(name);
        var identifier = xamlTypeDescriptor.Identifier + (isTypeMember ? string.Empty : (".$" + name));
        lock(LockCache)
        {
            if (MemberCache.ContainsKey(identifier)) return MemberCache[identifier];
            var memberDescriptor = new MemberDescriptor
            {
                Name = isTypeMember ? xamlTypeDescriptor.Name : name,
                        ImplementingType = xamlTypeDescriptor,
                        Identifier = identifier,
                        FullName = xamlTypeDescriptor.FullName + (isTypeMember ? string.Empty : (".$" + name)),
                        MemberType = isTypeMember ? MemberType.TypeReference : MemberType.Field,
            } ;
            MemberCache[identifier] = memberDescriptor;
            return memberDescriptor;
        }
    }

    public static TypeDescriptor BuildXamlTypeDescriptor(ModuleDescriptor module, string fileName)
    {
        var lastIndexOf = fileName.LastIndexOf('/');
        var typeName = fileName.Substring(lastIndexOf + 1).Replace(".baml", ".xaml");
        string path = module.Name;
        if (lastIndexOf > -1)
        {
            path += "." + fileName.Substring(0, lastIndexOf);
        }
        var submodule = BuildSubmoduleDescriptor(module, path);
        var identifier = submodule.Module.Identifier + " /type " + path + "." + typeName;
        lock(LockCache)
        {
            if (TypeCache.ContainsKey(identifier)) return TypeCache[identifier];
            var typeDescriptor = new TypeDescriptor
            {
                Name = "<XAML>" + typeName,
                        Submodule = submodule,
                        Identifier = identifier,
                        FullName = fileName,
                        AbstractionType = AbstractionType.Descriptive
            } ;
            TypeCache[identifier] = typeDescriptor;
            return typeDescriptor;
        }
    }
}